feat: implement DynamoDBDataSource + tests#534
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default mode and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d936b5d. Configure here.
| Error{"DynamoDB row missing expected 'item' attribute"}); | ||
| } | ||
|
|
||
| return SerializedItemDescriptor::Present(0, it->second.GetS()); |
There was a problem hiding this comment.
I wonder if we should type check or empty check calls to GetS? I have claude working on a test so I can understand scope a little easier.
| // first use and torn down during normal program termination via C++ static | ||
| // destruction. | ||
| // | ||
| // Static-destruction ordering caveat: if a caller stashes a raw AWS SDK |
There was a problem hiding this comment.
Do we need to surface this someplace more customer visible? Readme? Or just when we make docs?
There was a problem hiding this comment.
I guess I need to read further to understand if there is external impact. On some SDKs we allow passing clients, but they would then theoretically be non-owned. So more of an internal problem?
There was a problem hiding this comment.
Oh, the AWS Api is a bit icky here.
There was a problem hiding this comment.
I wonder if potentially we should have a way to disable the AWS SDK shutdown and let the customer be responsible for shutting it down?
|
From Claude.
Verified with two tests against Proposed fix and tests: // libs/server-sdk-dynamodb-source/src/dynamodb_source.cpp -- in Get(), after the
// "missing item attribute" check
auto const& serialized = it->second.GetS();
if (serialized.empty()) {
return tl::make_unexpected(
Error{"DynamoDB 'item' attribute is empty or not of type S"});
}
return SerializedItemDescriptor::Present(0, serialized);
// Same pattern inside All()'s per-row loop, applied to item_it->second.GetS().
// The third GetS() call (sort key, line 119) is left alone -- DynamoDB enforces
// the key schema at insert time, so it's guaranteed type S.Test helper added to void PutRowWithNumericItem(std::string const& ns_suffix,
std::string const& key) const {
Aws::DynamoDB::Model::PutItemRequest request;
request.SetTableName(table_name_);
request.AddItem("namespace",
Aws::DynamoDB::Model::AttributeValue{Prefixed(ns_suffix)});
request.AddItem("key", Aws::DynamoDB::Model::AttributeValue{key});
Aws::DynamoDB::Model::AttributeValue numeric_item;
numeric_item.SetN("12345");
request.AddItem("item", numeric_item);
auto outcome = client_.PutItem(request);
if (!outcome.IsSuccess()) {
FAIL() << "..." << outcome.GetError().GetMessage();
}
}Tests added to TEST_F(DynamoDBTests, GetReturnsErrorWhenItemAttributeIsNotString) {
WithPrefixedClient(prefix_, [&](auto const& client) {
client.PutRowWithNumericItem("features", "foo");
});
auto const result = source->Get(FlagKind{}, "foo");
ASSERT_FALSE(result);
}
TEST_F(DynamoDBTests, AllReturnsErrorWhenItemAttributeIsNotString) {
WithPrefixedClient(prefix_, [&](auto const& client) {
client.PutFlag(Flag{"foo", 1, true});
client.PutRowWithNumericItem("features", "bar");
});
auto const result = source->All(FlagKind{});
ASSERT_FALSE(result);
}Suite is 28/28 green with the fix applied. Reverting the source change makes only the two new tests red, with Broader hardening worth a separate PR: wrap |
DynamoDB does not enforce a type schema on non-key attributes, so a non-Relay writer (manual put-item, schema-migration tool, etc.) can produce a row whose 'item' attribute is stored as Number/Binary/Null/ Bool/List/Map instead of String. AttributeValue::GetS() on such a value silently returns an empty string. Without a check, the source returns Present(0, "") and the empty string flows into JsonDeserializer::DeserializeJsonDescriptor's throwing boost::json::parse, which propagates an uncaught exception out of the SDK's evaluation entry point. Treat an empty GetS() result as a structured error in both Get() and All(). The sort-key GetS() (All() line 119) is left untouched -- the DynamoDB service enforces the table's key schema at insert time, so the sort key is guaranteed type S by table-schema invariant. Adds PutRowWithNumericItem test helper that stores 'item' as a Number via AttributeValue::SetN, and two bug-proving tests (GetReturnsErrorWhenItemAttributeIsNotString, AllReturnsErrorWhenItemAttributeIsNotString) that fail without the fix and pass with it. Full dynamodb suite is 28/28 green. Per #534 (comment)
Ticket: SDK-2363 · Follows #534 ## Summary Adds the public `IBigSegmentStore` interface, the `Membership` / `StoreMetadata` value types, and concrete DynamoDB + Redis implementations. Schema strings match what the Relay Proxy writes. ```cpp auto redis_store = RedisBigSegmentStore::Create("redis://localhost:6379", "prefix"); auto dynamo_store = DynamoDBBigSegmentStore::Create("my-table", "prefix", options); ``` ## Design notes - **`synchronizedOn` parsing.** Stored as DynamoDB N / Redis string. Both stores reject malformed values (non-numeric strings, wrong DynamoDB attribute type) rather than silently returning 0, matching the existing `dynamodb_source.cpp` row validation. - **No hashing here.** The interface contract says callers pass the already-hashed base64 SHA-256 context key; the SDK will hash in the wrapper, not the store implementations. ## Not in scope - `BigSegmentsBuilder` config plumbing. - `BigSegmentStoreWrapper` (LRU cache + staleness polling) and the hashing path. - Evaluator wiring / replacing the `rules.cpp` big-segments TODO. ## Test plan - [x] 7 `Membership` unit tests pass. - [x] 9 `RedisBigSegmentStore` integration tests pass against Redis 7 (docker). - [x] 8 `DynamoDBBigSegmentStore` integration tests pass against DynamoDB Local. - [x] Full server-sdk test suite (468 tests) still green. - [x] Schema constants match what Relay writes (verified against the LocalStack/Redis fixture data). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > New external-store read path for segment targeting with strict schema validation; not yet wired into evaluation, so production flag behavior is unchanged until follow-up PRs land. > > **Overview** > Introduces a **Big Segments persistent store** surface for the server SDK: public `IBigSegmentStore`, inline `Membership` / `StoreMetadata`, and read-only **Redis** and **DynamoDB** backends that follow Relay’s key/schema layout (prefix scoping, include/exclude refs, sync metadata). > > Stores perform point lookups by opaque context hash, build membership via `Membership::FromSegmentRefs` (inclusion wins on overlap), and return errors on malformed DynamoDB attribute types or non-numeric sync timestamps instead of silent empty/zero results. **Config, caching wrapper, hashing, and flag evaluation wiring are not included** in this change. > > Integration tests cover membership/metadata behavior, prefix isolation, and error paths against Redis and DynamoDB Local. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d2622d9. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->

Summary
Implements
DynamoDBDataSource, similar toRedisDataSource, using the Proxy Relay schema as per the schema.Differences from the Redis source
All()paginates DynamoDB Query responses viaLastEvaluatedKey; Redishgetallis single-shot.InitAPI/ShutdownAPIare owned by an singleton (AwsSdkGuard); Redis++ needs no equivalent.DynamoDBClientOptionsstruct (region/endpoint/credentials) instead of a single URI string.ConsistentRead = trueto avoid DynamoDB's default eventually-consistent staleness (same as Java)features,$inited), matching what Relay writes per the spec. The Redis source unconditionally doesprefix + ":" + name, so an empty Redis prefix would produce leading-colon keys (:features) that no Relay deployment writes. That divergence is never exercised — Redis defaults to a non-empty prefix and the Redis tests always pass one, but wanted to note it.Test plan
amazon/dynamodb-localamazon/dynamodb-local; macOS / Windows build-only (matches Redis workflow).Note
Medium Risk
Adds a new DynamoDB-backed data source implementation plus integration tests and CI wiring, and adjusts CMake/AWS SDK build behavior (static vs shared), which could impact build/link outcomes across platforms.
Overview
Implements a real
DynamoDBDataSource(replacing the prior scaffold) that reads flags/segments from a Relay-compatible DynamoDB table, includingGet,All(with pagination), andInitializedchecks, with consistent-read requests and structured error handling for malformed rows.Introduces
DynamoDBClientOptionsplus internal helpers (AwsSdkGuard, client factory, namespace/prefix utilities) to manage AWS SDK lifecycle and client configuration without leaking AWS headers in the public API.Adds a new
teststarget with DynamoDB Local-backed coverage and updates CI/workflows and release scripts to enable DynamoDB builds/tests when requested; also forces the AWS SDK FetchContent build to static archives with cache save/restore to avoid macOS shared-link issues.Reviewed by Cursor Bugbot for commit c4f97d6. Bugbot is set up for automated code reviews on this repo. Configure here.